package com.piece.core.framework.util.file;

import com.piece.core.framework.enums.ContentType;
import com.piece.core.framework.enums.StatusType;
import com.piece.core.framework.properties.ApplicationProperties;
import com.piece.core.framework.support.convert.Convert;
import com.piece.core.framework.util.collection.ArrayUtil;
import com.piece.core.framework.util.basic.SpringUtil;
import com.piece.core.framework.util.string.StringUtil;
import com.piece.core.framework.util.net.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;

/**
 * 文件处理工具类
 */
@Slf4j
public class FileUtil extends org.apache.commons.io.FileUtils {

    private static List<String> IMAGE_TYPE = Arrays.asList(".gif", ".png", ".jpg", ".jpeg", ".bmp", ".ico", ".tiff", ".raw");

    private static List<String> PDF_TYPE = Arrays.asList(".pdf");

    private static List<String> DOC_TYPE = Arrays.asList(".doc", ".docx");

    private static List<String> EXCEL_TYPE = Arrays.asList(".xls", ".xlsx");

    private static List<String> PPT_TYPE = Arrays.asList(".ppt", ".pptx");

    private static List<String> TXT_TYPE = Arrays.asList(".txt", ".xml", ".yml", ".md", ".log", ".bat", ".properties");

    private static List<String> OFFICE_TYPE = Arrays.asList(".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx");

    private static List<String> VIDEO_TYPE = Arrays.asList(".mp4", ".wmv", ".h264", ".mov", ".avi", ".rm", ".rmvb", ".swf", ".mpeg");

    private static List<String> AUDIO_TYPE = Arrays.asList("mp3", "aac", "wav", "ogg");

    private static List<String> ARCHIVE_TYPE = Arrays.asList("rar", "zip", "jar", "7-zip", "7z", "tar", "gzip");

    private static Map<String, String> contentTypeMap = new HashMap<String, String>() {{
        put("pdf", "application/pdf");
        put("zip", "application/zip");
        put("gzip", "application/gzip");
        put("xls", "application/x-xls");
        put("xlsx", "application/x-xls");
        put("doc", "application/msword");
        put("docx", "application/msword");
        put("exe", "application/x-msdownload");
        put("ppt", "application/vnd.ms-powerpoint");
        put("pptx", "application/vnd.ms-powerpoint");
        put("ogg", "application/ogg");
        put("swf", "application/x-shockwave-flash");
        put("rm", "application/vnd.rn-realmedia");
        put("rmvb", "application/vnd.rn-realmedia-vbr");
        put("txt", "text/plain");
        put("gif", "image/gif");
        put("ico", "image/x-icon");
        put("jpg", "image/jpeg");
        put("jpeg", "image/jpeg");
        put("png", "image/png");
        put("bmp", "image/jpeg");
        put("tiff", "image/tiff");
        put("mp3", "audio/mp3");
        put("wav", "audio/wav");
        put("avi", "video/avi");
        put("mov", "video/mov");
        put("mp4", "video/mp4");
        put("mpeg", "video/mpg");
        put("wmv", "video/x-ms-wmv");
    }};

    /**
     * 根据不同浏览器生成下载文件名称
     */
    public static String generateDownloadFileName(HttpServletRequest request, String filename) {
        String Agent = request.getHeader("User-Agent");
        if (null != Agent) {
            try {
                Agent = Agent.toLowerCase();
                if (Agent.indexOf("firefox") != -1) {
                    filename = new String(filename.getBytes(), "iso8859-1");
                } else if (Agent.indexOf("msie") != -1) {
                    filename = URLEncoder.encode(filename, "utf-8");
                } else {
                    filename = URLEncoder.encode(filename, "utf-8");
                }
            } catch (Exception e) {
                log.error("生成文件名称异常：{}", e.getMessage());
            }
        }
        return filename;
    }

    /**
     * 生成唯一文件名
     */
    public static String generateUniqFileName(HttpServletRequest request) {
        String ip = IpUtil.getIp(request);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        String time = sdf.format(new Date());
        ip = ip.replace(".", "");
        return time + "-" + ip;
    }

    /**
     * 获取绝对路径
     *
     * @param dir
     * @param filename
     */
    public static final File getAbsoluteFile(String dir, String filename) throws IOException {
        // 获取项目路径
        Properties props = System.getProperties();
        String rootPath = props.getProperty("user.dir");
        File desc = new File(rootPath);
        dir = desc.getPath() + dir;
        desc = new File(dir + File.separator + filename);

        if (!desc.getParentFile().exists()) {
            desc.getParentFile().mkdirs();
        }
        if (!desc.exists()) {
            desc.createNewFile();
        }
        return desc;
    }

    /**
     * 从文件路径中获取上级路径地址
     */
    public static String getParentPath(String filePath) {
        return getParentPath(filePath, File.separator);
    }

    public static String getParentPath(String filePath, String separator) {
        return filePath.substring(0, filePath.lastIndexOf(separator));
    }

    /**
     * 从文件路径中获取文件名
     */
    public static String getFileName(String filePath) {
        return filePath.substring(filePath.lastIndexOf(File.separator) + 1);
    }

    /**
     * 从文件下载地址中获取文件名
     */
    public static String getFileNameFromUrl(String url) {
        if (url.indexOf("?") != -1) {
            String[] params = url.substring(url.indexOf("?") + 1).split("=");
            for (int i = 0; i < params.length; i++) {
                if (i < (params.length - 1) && "filename".equals(params[i])) {
                    String filename = params[i +1];
                    return filename.split("&")[0];
                }
            }
        }
        String nonPramStr = url.substring(0, url.indexOf("?") != -1 ? url.indexOf("?") : url.length());
        return nonPramStr.substring(nonPramStr.lastIndexOf(File.separator) + 1);
    }

    public static String getRootPath(String path, String root) {
        if (StringUtil.isEmpty(path) || path.equals("/")) {
            return root;
        }
        root = null == root ? "" : root;
        if (path.indexOf(root) != -1) {
            return path;
        }
        return root + path;
    }

    /**
     * 判断文件编码格式,GBK或UTF-8
     */
    public static String getFileEncodeUTFGBK(String filePath) {
        String enc = Charset.forName("GBK").name();
        File file = new File(filePath);
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            byte[] b = new byte[3];
            in.read(b);
            in.close();
            if (b[0] == -17 && b[1] == -69 && b[2] == -65) {
                enc = Charset.forName("UTF-8").name();
            }
        } catch (Exception e) {
            log.error("判断文件编码异常：{}", e.getMessage());
        }
        return enc;
    }

    /**
     * 获取文件大小
     */
    public static long getFileSize(String path) {
        long size = 0L;
        final File file = new File(path);
        if (!file.exists()) {
            return 0L;
        }
        if (file.isFile()) {
            // 文件
            try {
                size = Files.size(Paths.get(path));
            } catch (IOException e) {
                log.error("获取文件大小异常:{}", e.getMessage());
            }
        } else {
            // 目录
            File[] files = file.listFiles();
            for (File children : files) {
                size += getFileSize(children.getPath());
            }
        }
        return size;
    }

    public static long getNetFileSize(String url) {
        if (StringUtil.isEmpty(url)) {
            return 0;
        }
        HttpURLConnection conn = null;
        try {
            URL ur = new URL(url);
            conn = (HttpURLConnection) ur.openConnection();
            return conn.getContentLength();
        } catch (Exception e) {
            return 0;
        } finally {
            if (null != conn) {
                conn.disconnect();
            }
        }
    }

    /**
     * 把数据写至文本文件中
     *
     * @param filePath 文件路径
     * @param data     文本内容
     */
    public static void writeTextFile(String filePath, String data) {
        FileOutputStream fos = null;
        OutputStreamWriter writer = null;
        try {
            fos = new FileOutputStream(new File(filePath));
            writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
            writer.write(data);
        } catch (Exception e) {
            log.error("写入文本文件异常：{}", e.getMessage());
        } finally {
            try {
                if (null != writer) {
                    writer.close();
                }
                if (null != fos) {
                    fos.close();
                }
            } catch (Exception e) {
            }
        }
    }

    /**
     * 读取文本文件内容
     *
     * @param filePath 文件路径
     */
    public static String readTextFile(String filePath) {
        StringBuffer buffer = new StringBuffer();
        try {
            File file = new File(filePath);
            return readTextFile(file);
        } catch (Exception e) {
            log.error("未找到文本文件[{}]", filePath);
        }
        return buffer.toString();
    }

    /**
     * 读取文本文件内容
     */
    public static String readTextFile(File file) {
        StringBuffer buffer = new StringBuffer();
        try {
            FileInputStream fis = null;
            BufferedReader breader = null;
            try {
                fis = new FileInputStream(file);
                InputStreamReader isReader = new InputStreamReader(fis, StandardCharsets.UTF_8);
                breader = new BufferedReader(isReader);
                String line;
                while (null != (line = breader.readLine())) {
                    buffer.append(line);
                    buffer.append("\r\n");
                }
                breader.close();
                isReader.close();
                fis.close();
            } catch (IOException e) {
                log.error("读取文本文件异常：{}", e.getMessage());
            }
        } catch (Exception e) {
            log.error("读取文本文件错误：{}", e.getMessage());
        }
        return buffer.toString();
    }

    /**
     * 读取文本文件内容
     */
    public static String readTextFile(InputStream is) {
        StringBuffer buffer = new StringBuffer();

        try {
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] bytes=new byte[1024];
            int len;
            while ((len=is.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
            }
            is.close();
            buffer.append(outputStream.toString());
            outputStream.close();
            return buffer.toString();
        } catch (IOException e) {
            log.error("读取文本文件异常：{}", e.getMessage());
        }
        return "";
    }

    /**
     * 获取文件名的前缀
     */
    public static String getFilePrefix(String fileName) {
        int splitIndex = fileName.lastIndexOf(".");
        if (splitIndex == -1) return fileName;
        else return fileName.substring(0, splitIndex);
    }

    /**
     * 获取文件名的后缀
     */
    public static String getFileSuffix(String fileName) {
        int splitIndex = fileName.lastIndexOf(".");
        if (splitIndex == -1) return "";
        else return fileName.substring(splitIndex + 1);
    }

    public static String getFileNameFromPath(String filePath) {
        String[] pathArr = filePath.split(File.separator);
        String fileName;
        if (pathArr.length > 1) {
            fileName = pathArr[pathArr.length - 1];
        } else {
            fileName = filePath;
        }
        return fileName;
    }

    public static String getFileSuffixFromPath(String filePath) {
        String fileName = getFileNameFromPath(filePath);
        String[] arr = fileName.split("\\.");
        return arr[arr.length - 1];
    }

    /**
     * 创建文件目录
     */
    public static boolean makeDir(String dir) {
        File file = new File(dir);
        if (!file.exists()) {
            return file.mkdirs();
        }
        return true;
    }

    /**
     * 删除文件
     */
    public static void deleteFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            if (file.isDirectory()) {
                deleteFiles(filePath);
            }
            file.delete();
        }
    }

    /**
     * 删除文件夹下所有文件
     */
    public static void deleteFiles(String filePath) {
        File file = new File(filePath);
        File[] fileList = file.listFiles();
        String dirPath = null;
        if (ArrayUtil.isNotEmpty(fileList)) {
            for (int i = 0; i < fileList.length; i++) {
                if (fileList[i].isDirectory()) {
                    dirPath = fileList[i].getPath();
                    deleteFiles(dirPath);
                }
                fileList[i].delete();
            }
        }
        file.delete();
    }

    /**
     * 读取文件
     */
    public static byte[] toByte(InputStream is) throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] bytes = new byte[0];
        try {
            byte[] buff = new byte[1024];
            int rc = 0;
            while ((rc = is.read(buff, 0, 1024)) > 0) {
                output.write(buff, 0, rc);
            }
            bytes = output.toByteArray();
        } catch (IOException e) {
            log.error("读取文件到byte数组异常：{}", e.getMessage());
        } finally {
            output.close();
        }
        return bytes;
    }

    /**
     * 写入文件
     */
    public static void write(File file, InputStream is) throws Exception {
        BufferedInputStream in = null;
        BufferedOutputStream os = null;
        try {
            in = new BufferedInputStream(is);
            os = new BufferedOutputStream(new FileOutputStream(file));
            int len = -1;
            byte[] b = new byte[1024];
            while ((len = in.read(b)) != -1) {
                os.write(b, 0, len);
            }
        } catch (Exception e) {
            log.error("写入文件异常：{}", e.getMessage());
            throw new Exception(e);
        } finally {
            if (null != in) {
                in.close();
            }
            if (null != os) {
                os.close();
            }
        }
    }

    /**
     * 写入文件
     *
     * @param pathAndName 文件名
     * @param bytes
     * @return 文件路径
     */
    public static String write(byte[] bytes, String pathAndName) throws IOException {
        return write(bytes, pathAndName, true);
    }

    /**
     * 写入文件
     *
     * @param bytes
     * @param pathAndName 文件名
     * @param stat        是否使用默认上传文件路径
     * @return 文件路径
     */
    public static String write(byte[] bytes, String pathAndName, boolean stat) throws IOException {
        String dest = stat ? getStoragePath() + pathAndName : pathAndName;
        writeByteArrayToFile(new File(dest), bytes);
        return dest;
    }

    /**
     * 图片类型判断
     */
    public static boolean checkImageType(String fileName) {
        return checkFileType(fileName, IMAGE_TYPE.iterator());
    }

    /**
     * 文本文档类型判断
     */
    public static boolean checkTxtType(String fileName) {
        return checkFileType(fileName, TXT_TYPE.iterator());
    }

    /**
     * Pdf文档类型判断
     */
    public static boolean checkPdfType(String fileName) {
        return checkFileType(fileName, PDF_TYPE.iterator());
    }

    /**
     * Doc文档类型判断
     */
    public static boolean checkDocType(String fileName) {
        return checkFileType(fileName, DOC_TYPE.iterator());
    }

    /**
     * Excel文档类型判断
     */
    public static boolean checkExcelType(String fileName) {
        return checkFileType(fileName, EXCEL_TYPE.iterator());
    }

    /**
     * PPT文档类型判断
     */
    public static boolean checkPPTType(String fileName) {
        return checkFileType(fileName, PPT_TYPE.iterator());
    }

    /**
     * Office文档类型判断
     */
    public static boolean checkOfficeType(String fileName) {
        return checkFileType(fileName, OFFICE_TYPE.iterator());
    }

    /**
     * 视频类型判断
     */
    public static boolean checkVideoType(String fileName) {
        return checkFileType(fileName, VIDEO_TYPE.iterator());
    }

    /**
     * 音频类型判断
     */
    public static boolean checkAudioType(String fileName) {
        return checkFileType(fileName, AUDIO_TYPE.iterator());
    }

    /**
     * 压缩包类型判断
     */
    public static boolean checkArchiveType(String fileName) {
        return checkFileType(fileName, ARCHIVE_TYPE.iterator());
    }

    private static boolean checkFileType(String fileName, Iterator<String> type) {
        while (type.hasNext()) {
            String ext = type.next();
            if (fileName.toLowerCase().endsWith(ext)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 字节转换
     *
     * @param size 字节大小
     * @return 转换后值
     */
    public static String convertFileSize(long size) {
        long kb = 1024;
        long mb = kb * 1024;
        long gb = mb * 1024;
        if (size >= gb) {
            return String.format("%.1f GB", (float) size / gb);
        } else if (size >= mb) {
            float f = (float) size / mb;
            return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
        } else if (size >= kb) {
            float f = (float) size / kb;
            return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
        } else {
            return String.format("%d B", size);
        }
    }

    /**
     * 浏览器直接下载文件
     *
     * @param request  request
     * @param response response
     * @param file     文件
     * @param fileName 文件名
     */
    public static void downloadForWeb(HttpServletRequest request, HttpServletResponse response, File file, String fileName) throws Exception {
        downloadForWeb(request, response, new FileInputStream(file), fileName);
    }

    /**
     * 浏览器直接下载文件
     *
     * @param request  request
     * @param response response
     * @param is       文件流
     * @param fileName 文件名
     */
    public static void downloadForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String fileName) throws Exception {
        fileName = generateDownloadFileName(request, fileName);
        FileUtil.beforeDownload(request, response, fileName, ContentType.STREAM.toString(StandardCharsets.UTF_8));
        OutputStream os = response.getOutputStream();
        try {
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
        } catch (IOException e) {
            // 在写数据的时候， 对于 ClientAbortException 之类的异常，是因为客户端取消了下载，而服务器端继续向浏览器写入数据时， 抛出这个异常。
        } finally {
            FileUtil.afterDownload(response, is, os);
        }
    }

    /**
     * 浏览器播放音频文件
     *
     * @param request  request
     * @param response response
     * @param is       文件流
     * @param filename 文件名
     * @param fileSize 文件大小
     */
    public static void audioForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename, long fileSize) throws Exception {
        String contentType = getContentType(filename, ContentType.MP3.toString(StandardCharsets.UTF_8));
        FileUtil.beforeDownload(request, response, null, contentType);
        multiForWeb(request, response, is, filename, fileSize);
        response.flushBuffer();
        response.setHeader("Connection", "close");
    }

    /**
     * 浏览器播放视频文件
     *
     * @param request  request
     * @param response response
     * @param is       文件流
     * @param filename 文件名
     * @param fileSize 文件大小
     */
    public static void videoForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename, long fileSize) throws Exception {
        String contentType = getContentType(filename, ContentType.MP4.toString(StandardCharsets.UTF_8));
        FileUtil.beforeDownload(request, response, null, contentType);
        multiForWeb(request, response, is, filename, fileSize);
        response.flushBuffer();
        response.setHeader("Connection", "close");
    }

    /**
     * 浏览器直接展示图片
     *
     * @param request  request
     * @param response response
     * @param is       文件流
     * @param filename 文件名
     */
    public static void imageForWeb(final HttpServletRequest request, final HttpServletResponse response, InputStream is, String filename) throws Exception {
        String contentType = getContentType(filename, ContentType.JEPG.toString(StandardCharsets.UTF_8));
        FileUtil.beforeDownload(request, response, null, contentType);
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        OutputStream os = response.getOutputStream();
        exportForWeb(is, os);
        FileUtil.afterDownload(response, is, os);
    }

    /**
     * 浏览器预览文件
     */
    public static void txtForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename) throws Exception {
        String contentType = getContentType(filename, ContentType.TEXT_HTML.toString(StandardCharsets.UTF_8));
        FileUtil.beforeDownload(request, response, null, contentType);
        OutputStream os = response.getOutputStream();
        exportForWeb(is, os);
        FileUtil.afterDownload(response, is, os);
    }

    /**
     * 浏览器预览文件
     */
    public static void objectForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename) throws Exception {
        String contentType = getContentType(filename, ContentType.TEXT_PLAIN.toString(StandardCharsets.UTF_8));
        FileUtil.beforeDownload(request, response, null, contentType);
        OutputStream os = response.getOutputStream();
        exportForWeb(is, os);
        FileUtil.afterDownload(response, is, os);
    }

    public static void exportForWeb(InputStream is, OutputStream os) {
        try {
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
        } catch (IOException e) {
            log.error("下载文件异常：{}", e.getMessage());
        }
    }

    /**
     * 组装下载文件请求头
     */
    public static void beforeDownload(HttpServletRequest request, HttpServletResponse response, String fileName, String contentType) throws Exception {
        request.setCharacterEncoding("utf-8");
        response.reset();
        //response.setHeader("Access-Control-Allow-Origin", request.getHeader("HTTP_ORIGIN"));
        response.setHeader("Access-Control-Allow-Origin", "*");
        if (null != fileName) {
            response.setHeader("Download-Filename", fileName);
            response.setHeader("Access-Control-Expose-Headers", "Download-Filename");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        }
        if (null != contentType) {
            response.setContentType(contentType);
        }
    }

    /**
     * 组装下载文件响应头
     */
    public static void afterDownload(HttpServletResponse response, InputStream is, OutputStream os) {
        try {
            if (null != os)
                os.close();
            if (null != is)
                is.close();
            response.flushBuffer();
            response.setHeader("Connection", "close");
        } catch (IOException e) {
            // 在写数据的时候， 对于 ClientAbortException 之类的异常，是因为客户端取消了下载，而服务器端继续向浏览器写入数据时， 抛出这个异常。
        }
    }

    /**
     * 下载网络文件到服务器
     *
     * @param savePath 保存地址
     * @param fileUrl  网络文件地址
     * @param delete   是否删除历史文件
     * @return
     */
    public static StatusType downloadFile(String savePath, String fileUrl, boolean delete) {
        URL urlfile = null;
        HttpURLConnection httpUrl = null;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        if (fileUrl.startsWith("//")) {
            fileUrl = "http:" + fileUrl;
        }
        String fileName;
        try {
            urlfile = new URL(fileUrl);
            String urlPath = urlfile.getPath();
            fileName = urlPath.substring(urlPath.lastIndexOf("/") + 1);
        } catch (MalformedURLException e) {
            log.error("URL异常", e);
            return StatusType.FAIL;
        }

        File path = new File(savePath);
        if (!path.exists()) {
            path.mkdirs();
        }
        File file = new File(savePath + File.separator + fileName);
        if (file.exists()) {
            if (delete) {
                file.delete();
            } else {
                return StatusType.SUCCESS;
            }
        }
        try {
            httpUrl = (HttpURLConnection) urlfile.openConnection();
            httpUrl.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0");
            // 读取超时时间
            httpUrl.setReadTimeout(60000);
            // 连接超时时间
            httpUrl.setConnectTimeout(60000);
            httpUrl.connect();
            bis = new BufferedInputStream(httpUrl.getInputStream());
            bos = new BufferedOutputStream(new FileOutputStream(file));
            int len = 2048;
            byte[] b = new byte[len];
            long readLen = 0;
            while ((len = bis.read(b)) != -1) {
                bos.write(b, 0, len);
            }
            bos.flush();
            bis.close();
            httpUrl.disconnect();
            return StatusType.SUCCESS;
        } catch (SocketTimeoutException e) {
            log.error("读取文件超时", e);
            return StatusType.FAIL;
        } catch (Exception e) {
            log.error("远程文件下载失败", e);
            return StatusType.FAIL;
        } finally {
            try {
                if (null != bis) {
                    bis.close();
                }
                if (null != bos) {
                    bos.close();
                }
            } catch (Exception e) {
                log.error("下载出错", e);
            }
        }
    }

    /**
     * 多线程下载文件，支持断点续传
     *
     * @param request
     * @param response
     * @param is       文件流
     * @param filename 文件名
     * @param fileSize 文件大小
     */
    public static void multiExportForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename, long fileSize) throws Exception {
        String contentType = getContentType(filename, "application/octet-stream;charset=UTF-8");
        FileUtil.beforeDownload(request, response, null, contentType);
        multiForWeb(request, response, is, null, fileSize);
        response.flushBuffer();
        response.setHeader("Connection", "close");
    }

    /**
     * 大文件多线程下载，断点续传
     */
    private static void multiForWeb(HttpServletRequest request, HttpServletResponse response, InputStream is, String filename, long fileLength) {
        OutputStream os = null;
        BufferedInputStream bis = null;

        try {
            long p = 0L;
            long toLength = 0L;
            long contentLength = 0L;
            int rangeSwitch = 0;
            // 0,从头开始的全文下载；1,从某字节开始的下载（bytes=27000-）；2,从某字节开始到某字节结束的下载（bytes=27000-39000）
            String rangeBytes = "";
            bis = new BufferedInputStream(is);

            // tell the client to allow accept-ranges
            response.reset();
            response.setHeader("Accept-Ranges", "bytes");
            // client requests a file block download start byte
            String range = request.getHeader("Range");

            if (null != range && range.trim().length() > 0 && !"null".equals(range)) {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                rangeBytes = range.replaceAll("bytes=", "");
                if (rangeBytes.endsWith("-")) {
                    // bytes=270000-
                    rangeSwitch = 1;
                    p = Long.parseLong(rangeBytes.substring(0, rangeBytes.indexOf("-")));
                    contentLength = fileLength - p;
                } else {
                    // bytes=270000-320000
                    rangeSwitch = 2;
                    String temp1 = rangeBytes.substring(0, rangeBytes.indexOf("-"));
                    String temp2 = rangeBytes.substring(rangeBytes.indexOf("-") + 1);
                    p = Long.parseLong(temp1);
                    toLength = Long.parseLong(temp2);
                    contentLength = toLength - p + 1;
                }
            } else {
                contentLength = fileLength;
            }

            // Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节]
            response.setHeader("Content-Length", new Long(contentLength).toString());

            // 响应的格式:
            // Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小]
            if (rangeSwitch == 1) {
                String contentRange = new StringBuffer("bytes ").append(new Long(p))
                        .append("-").append(new Long(fileLength - 1)).append("/")
                        .append(new Long(fileLength)).toString();
                response.setHeader("Content-Range", contentRange);
                bis.skip(p);
            } else if (rangeSwitch == 2) {
                String contentRange = range.replace("=", " ") + "/" + new Long(fileLength);
                response.setHeader("Content-Range", contentRange);
                bis.skip(p);
            } else {
                String contentRange = new StringBuffer("bytes ").append("0-").append(fileLength - 1).append("/")
                        .append(fileLength).toString();
                response.setHeader("Content-Range", contentRange);
            }

            os = response.getOutputStream();
            int n = 0;
            long readLength = 0;
            int bsize = 1024;
            byte[] bytes = new byte[bsize];
            if (rangeSwitch == 2) {
                // 针对 bytes=27000-39000 的请求，从27000开始写数据
                while (readLength <= contentLength - bsize) {
                    n = bis.read(bytes);
                    readLength += n;
                    os.write(bytes, 0, n);
                }

                if (readLength <= contentLength) {
                    n = bis.read(bytes, 0, (int) (contentLength - readLength));
                    os.write(bytes, 0, n);
                }
            } else {
                while ((n = bis.read(bytes)) != -1) {
                    os.write(bytes, 0, n);
                }
            }
        } catch (IOException e) {
            // 在写数据的时候， 对于 ClientAbortException 之类的异常，是因为客户端取消了下载，而服务器端继续向浏览器写入数据时， 抛出这个异常。
        } catch (Exception e) {
            log.error("多线程下载文件异常：{}", e.getMessage());
        } finally {
            try {
                os.flush();
                os.close();
                bis.close();
                is.close();
            } catch (IOException e) {
                // 在写数据的时候， 对于 ClientAbortException 之类的异常，是因为客户端取消了下载，而服务器端继续向浏览器写入数据时， 抛出这个异常。
            }
        }
    }

    public static String getContentType(String filename, String defaultContentType) {
        String contentType = contentTypeMap.get(getFileSuffix(filename));
        return null == contentType ? defaultContentType : contentType + ";charset=UTF-8";
    }

    public static String getContentType(byte[] header) {
        String fileHeader = bytesToHexString(header).toUpperCase();
        switch (fileHeader) {
            case "FFD8FF":
            case "474946":
            case "89504E":
            case "49492A":
            case "424D":
                return "picture";
            case "C3F786":
            case "68746D":
                return "simText";
            case "D0CF11":
                return "office";
            case "255044":
                return "pdf";
            case "57415645":
            case "41564920":
            case "2E524D":
            case "000001":
            case "6D6F6F76":
                return "media";
            case "504B0304":
            case "52617221":
                return "compress";
            default:
                return "other";
        }
    }

    public static String getMimeType(String fileName) {
        return getMimeType(fileName, ContentType.STREAM.getValue());
    }

    public static String getMimeType(String fileName, String defaultMimeType) {
        String contentType = URLConnection.getFileNameMap().getContentTypeFor(fileName);
        if (null == contentType) {
            if (fileName.endsWith(".css")) {
                contentType = "text/css";
            } else if (fileName.endsWith(".js")) {
                contentType = ContentType.JAVASCRIPT.getValue();
            }
        }

        if (StringUtil.isEmpty(contentType)) {
            return defaultMimeType;
        }

        return contentType;
    }

    public static String md5(byte[] bytes) {
        byte[] hash;
        try {
            hash = MessageDigest.getInstance("MD5").digest(bytes);
        } catch (NoSuchAlgorithmException var7) {
            throw new RuntimeException("MessageDigest不支持MD5Util", var7);
        }

        StringBuilder hex = new StringBuilder(hash.length * 2);
        byte[] var3 = hash;
        int var4 = hash.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            byte b = var3[var5];
            if ((b & 255) < 16) {
                hex.append("0");
            }

            hex.append(Integer.toHexString(b & 255));
        }

        return hex.toString();
    }

    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder();
        if (null == src || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    public static boolean merge(String filePath, String folder, String fileName) {
        if (exists(filePath)) {
            try {
                Files.delete(Paths.get(folder));
            } catch (IOException e) {
                log.error("删除分片文件夹失败", e);
            }
            return true;
        }

        try {
            Files.createFile(Paths.get(filePath));
            Files.list(Paths.get(folder))
                    //.filter(path -> !path.getFileName().toString().equals(fileName))
                    .sorted((o1, o2) -> {
                        String p1 = o1.getFileName().toString();
                        String p2 = o2.getFileName().toString();
                        int i1 = Convert.toInt(p1);
                        int i2 = Convert.toInt(p2);
                        return i1 - i2;
                    }).forEach(path -> {
                        try {
                            // 以追加的形式写入文件
                            Files.write(Paths.get(filePath), Files.readAllBytes(path), StandardOpenOption.APPEND);
                            // 合并后删除该块
                            Files.delete(path);
                        } catch (IOException e) {
                            log.error("合并分片文件失败", e);
                        }
                    });
            return true;
        } catch (IOException e) {
            log.error("合并分片文件失败", e);
            return false;
        }
    }

    public static boolean exists(String filePath) {
        Path path = Paths.get(filePath);
        return Files.exists(path,new LinkOption[]{ LinkOption.NOFOLLOW_LINKS });
    }

    public static String getSystemCharset() {
        String os = System.getProperty("os.name");
        if (os != null && os.toLowerCase().startsWith("windows")) {
            //Windows操作系统
            return "GBK";
        } else if (os != null && os.toLowerCase().startsWith("linux")) {
            //Linux操作系统
            return "utf-8";
        } else { //其它操作系统
            return Charset.defaultCharset().name();
        }
    }

    public static String getStoragePath() {
        return getStoragePath(null);
    }

    public static String getTempPath() {
        return getStoragePath("temp");
    }

    public static String getTemplatePath() {
        return getStoragePath("template");
    }

    public static String getStoragePath(String path) {
        try {
            ApplicationProperties properties = SpringUtil.getBean(ApplicationProperties.class);
            // 获取项目路径
            Properties props = System.getProperties();
            String rootPath = props.getProperty("user.dir");
            File desc = new File(rootPath);
            String upload = properties.getUpload();
            if (StringUtil.isNotEmpty(path)) {
                upload += File.separator + path;
            }
            return desc + File.separator + upload.replaceAll("/", Matcher.quoteReplacement(File.separator)) + File.separator;
        } catch (Exception e) {
            return File.separator;
        }
    }

    public static InputStream readSystemFile(String resourceLocation) throws IOException {
        if (resourceLocation.startsWith("classpath")) {
            resourceLocation = resourceLocation.substring(resourceLocation.indexOf(":") + 1);
        }
        ClassPathResource resource = new ClassPathResource(resourceLocation);
        return Optional.ofNullable(resource.getInputStream()).orElse(null);
    }
}
